(PART) Recommendation system

Recommendation system

Based on some studies it has been proven that personalized product recommendations drive 24% of the orders and 26% of the revenue. This explains the influence recommendation has on volume of orders and generally on sales figures. What is more, it has been proven that product recommendations lead to reoccurring visits and that purchases on recommendation mark higher average-order value. Consequently, we decided to use method called user-based collaborative filtering to build our recommendation system (Reference).

First, we proceed with data preparation and pre-processing, then we build our recommender system, and finally draw business implications.

Data collection

As we earlier mentioned, we use data on Amazon customer reviews of beauty products. The data used in this project can be accessed in this link. It contains the following features:

Variable Description
Product price How much a product costs.
Product ID ASIN number of a product on Amazon.
Product title Name of a product.
Review helpfulness Fraction of users who found the review helpful.
Profile name Name of the profile on Amazon.
Review score Rating of the product.
Review summary Concise summary of the review text.
Review text Review text.
Review time Review time.
Review userId Review userId
Note: Information collected from http://snap.stanford.edu/data/web-Amazon-links.html

Data preparation and preprocessing

Packages

#Packages
library(R.utils)
library(dplyr)
library(tidyr)
library(janitor)
library(recommenderlab)
library(tm)
library(NLP)
library(qdap)
library(readr)
library(wordcloud)

Data collection

After downloading data locally we load in data by usingreadLines() function:

# Loading in data
my_data <- readRDS("data/amazon_beauty_full.RDS")

Let us first have a look at the dimension of our data. Our data set is currently in a form of a single vector with 2772616 elements. Obviously, this is not the optimal form of the data we would like to work with. That is why we need to work around this data set to make it more convenient for further analysis.

What we can do first is to remove all fields with no characters:

my_data <- my_data[sapply(my_data, nchar) > 0]

Then we can convert it to data frame:

my_data <- as.data.frame(my_data)
colnames(my_data) <- "product"

One of the critical steps is separating the column to multiple columns:

# Separate one column to two (":" separator)
my_data <- separate(my_data,col = product, into = c("Info","Product"), sep = ":")

Inspecting first 10 values:

head(my_data,10)

The data set is loaded in .txt format, which makes it a bit challenging to work with. In the following sections we will undertake data manipulation in order to bring the data set in more suitable form.

First, we will convert it from the current long-format to the wide-format, where each column will represent a product, and each row a feature:

#Converting long format to wide
my_data <- my_data %>%
  group_by(Info) %>%
  mutate(Order = seq_along(Info)) %>%
  spread(key = Order, value = Product)

Since the column names are labeled with numbers, we will apply first row as a label for the corresponding column name:

my_data <- as.data.frame(t(my_data))
my_data<-my_data%>%
  row_to_names(row_number = 1)
my_data

Delete rows with at least 1 NAs:

my_data <- my_data[rowSums(is.na(my_data))==0,]

Trim white space at the beginning or ending the string:

my_data$`review/userId`<- trimws(my_data$`review/userId`)
my_data$`product/productId`<- trimws(my_data$`product/productId`)
my_data$`product/price`<- trimws(my_data$`product/price`)
my_data$`product/title`<- trimws(my_data$`product/title`)

Filtering out reviews with unknown userID and productId:

my_data<-filter(my_data,`review/userId`!="unknown" & `product/productId`!="unknown" & `product/price`!="unknown")
my_data

Correcting column classes:

my_data$`product/productId` <- as.factor(my_data$`product/productId`)
table(my_data$`review/score`)

   1    2    3    4    5 
 814  424  580 1209 5554 

How many times users reviewed products?

In order to use relevant data, we would need to define the minimum number of reviews per user. Since majority of users left only one review. Therefore, we will remove all single-review users and all other users who left less then 2 reviews.

Filtering out users who left 2 or more reviews:

freq<-as.data.frame(table(my_data$`review/userId`))
freq
index<-filter(freq, freq$Freq>1)$Var1

We are now left with 1316 users who reviewed certain beauty product at least 2 times.

#my_data <- subset(my_data,`review/userId` %in% index)
my_data

Exploratory data analysis

Head of data

head(my_data)

How many unique products are reviewed?

length(unique(my_data$`product/productId`))
[1] 928

There are 928 products which were reviewed.

How many reviewers do we have?

length(unique(my_data$`review/userId`))
[1] 8002

There are 8002 unique reviewers/customers who reviewed products.

How many scores do we have?

length(my_data$`review/score`)
[1] 8581

There are 8581 ratings.

What is the distribution of ratings?

hist(as.numeric(my_data$`review/score`),main = "Histogramm of scores",xlab = "Score")

Products seem to be favorably rated as the distribution of scores showes that the best score is the most frequent.

What is the average number of reviews per user?

my_data %>% 
  group_by(`review/userId`) %>%
  summarise(Freq=n())%>% 
  select(Freq) %>% 
  summary()
      Freq       
 Min.   : 1.000  
 1st Qu.: 1.000  
 Median : 1.000  
 Mean   : 1.072  
 3rd Qu.: 1.000  
 Max.   :37.000  

In the original data set It users left on average left a review only once. After filtering, we see that our average is at 3 reviews per user.

What is the average score per user?

(grand.mean <- my_data %>% 
 dplyr::summarise(Grand.mean=mean(`review/score`)))
It seems that beauty products on Amazon are well received by users as the average score per user is quite high, at

.

Building a model

Final data outlook

Here is a glimpse in our data before we start building the recommnder:

head(my_data)

Subsetting data

In order to model a recommender system, three variables in our case are of great importance:

  • User ID
  • Product ID
  • Score / Rating

Our model will be based on these three variables. Additionally, we will make use of the remaining features by utilizing some text mining techniques, but you will find more details at some later point. Now, we will make a subset of our data with 3 mentioned variables:

table(subset_my_data$`review/score`)

   1    2    3    4    5 
 814  424  580 1209 5554 

Let us inspect the dimensions:

dim(subset_my_data)
[1] 8581    3

Formatting data

Our data is currently in the long format, i.e. one row for one rating. However, we would want to get a matrix with ratings where the rows represent the users IDs and the columns the Product IDs. Thus, we will transform our data to so called rating matrix:

ratings <- as(subset_my_data, "realRatingMatrix")

Inspecting real rating matrix

vector_ratings <- as.vector(ratings@data)
table(vector_ratings)
vector_ratings<-vector_ratings[vector_ratings != c(0,6,8,9,10,15,16,20,27)]
# Most reviewed products
colCounts(ratings)
# Average rating per product
colMeans(ratings)

In order to avoid “high/low rating bias” from users who give high (or low) ratings to all the products they reviewed, we will need to normalize our data. That would prevent certain bias in the results.

#ratings <- normalize(ratings)

We can plot an image of the rating matrix for the first 250 users and 250 products:

image(ratings[1:250,1:250])

From the visualisation we can see that rating matrix is very sparse, i.e. that not every user did rate/review every product in our data set.

We can inspect the data for the first 10 users and the first 4 products:

ratings[1:10, 1:4]@data
10 x 4 sparse Matrix of class "dgCMatrix"
                      B000052Z5B B000052Z5L B000052Z5M B000052Z89
A00275441WYR3489IKNAB          .          .          .          .
A0353671240B3B6L8WKZB          .          .          .          .
A0793784FP3F6ZXZDTN6           .          .          .          .
A10013UITIMJVI                 .          .          .          .
A1008GFLTBL76H                 .          .          .          .
A100VLYGYI6FXY                 .          .          .          .
A100W0JWG5GB6G                 .          .          .          .
A1016Z89IM29SK                 .          .          .          .
A102NKLXRT5KEM                 .          .          .          .
A102U9TVYZC0DX                 .          .          .          .

As we already saw in the visualisation, the data is sparse and the first 10 users did not review first 4 products visualised in the matrix above.

vector_ratings<-as.vector(ratings@data)
unique(vector_ratings)
 [1]  0  4  5  2  3  1 10  9  8 15  6 20 16 27
table(vector_ratings)
vector_ratings
      0       1       2       3       4       5       6       8       9      10      15      16      20      27 
7417340     808     422     576    1193    5472       3       4       2      27       5       1       2       1 

Comparing methods

Evaluating ratings

n_fold<-5
items_to_keep<-1
rating_threshold <- 3
eval_sets <- evaluationScheme(data=ratings,method="cross-validation", k=n_fold, given=items_to_keep,goodRating=rating_threshold)

Comparing methods

models_to_eval <- list(
  UBCF_cos =list(name="UBCF",param=list(method="cosine")),
  UBCF_cor =list(name="UBCF",param=list(method="pearson")))

n_recommendation <- c(1,5,10,100)


list_eval <- evaluate(eval_sets,method = models_to_eval, n=n_recommendation )
UBCF run fold/sample [model time/prediction time]
     1  
Error in neighbors[, x] : incorrect number of dimensions
UBCF run fold/sample [model time/prediction time]
     1  
Error in neighbors[, x] : incorrect number of dimensions

Building a recommender

Finally, we will now build our recommendation system based on User-based collaborative filtering User-based collaborative filtering search for similar users and gives them recommendations based on what other users with similar rating patterns appreciated:

getModel(recommender)
$description
[1] "UBCF-Real data: contains full or sample of data set"

$data
8002 x 928 rating matrix of class ‘realRatingMatrix’ with 8516 ratings.
Normalized using center on rows.

$method
[1] "cosine"

$nn
[1] 25

$sample
[1] FALSE

$weighted
[1] TRUE

$normalize
[1] "center"

$min_matching_items
[1] 0

$min_predictive_items
[1] 0

$verbose
[1] FALSE

Additionally, in order to compare results of two methods, we would like to apply item-based collaborative filtering method to build another recommender system. In contrast to user-based collaborative filtering, item-based collaborative filtering looks for similarity patterns between items and recommends them to users based on the computed information.

recommenderIBCF <- Recommender(ratings, method="IBCF")
recommenderIBCF

As reported, both recommendation systems are built using 8002 users.

Interpretation and managerial implications

Now we would like to interpret the output of our recommender systems. First we start with UBCF-based recommender system.

current.user <- 45
recommendations <- predict(recommender, current.user, data = ratings, n = 5)

We decided to take user number 45 and inspect 5 recommendations provided to him/her. Now we can inspect what our recommendation system provided in the end:

str(recommendations)

We can see that the user ID of the user number 45 is A10N19OL0CKYDV. Our system found 2 products to recommend to this user, and we can find product index (173, 772) as well as ratings that the system calculated from the ratings of the closest users (5,5).

Let us create a prediction made by IBCF-based recommender:

recommendationsIBCF <- predict(recommenderIBCF,current.user,data = ratings, n=5)
str(recommendationsIBCF)

We will inspect potential recommended products:

head(as(recommendationsIBCF,"list"))

Unfortunately, our item-based collaborative filtering system did not generate any recommendation for the user number 45.

Implications

As we could see, this user reviewed only one product, called “Opi Ridge Filler .5 oz.”, and it is a nail-care product. We could assume that this person is a female user since the product she bought is typically associated with female beauty care. What is more, two recommended products are as well very strongly associated to being typical female beauty products. Finally, we have the name of the user (Erica), so we can be sure that the user is a female. From the qualitative perspective it seems that our recommendation system provides descent recommendations!.

Bonus analysis: Text Mining

In addition to our recommender system, we will apply some basic text mining techniques to explore reviews text. Text mining helps us to mine opinions of users (in this case) about the reviewed products at scale.

Wordcloud

Here we create a wordcloud of words from product reviews of recommended products to the user 45. Beforehand we would need to pre-process the text of reviews in the following manner:

# Split text into parts using new line character:
text.docs <- Corpus(VectorSource(recommendation_26$`review/text`))
toSpace <- content_transformer(function (x , pattern ) gsub(pattern, " ", x))
text.docs <- tm_map(text.docs, toSpace, "/")
text.docs <- tm_map(text.docs, toSpace, "@")
text.docs <- tm_map(text.docs, toSpace, "\\|")
text.docs <- tm_map(text.docs, content_transformer(tolower))
text.docs <- tm_map(text.docs, removeNumbers)
text.docs <- tm_map(text.docs, stripWhitespace)
text.docs <- tm_map(text.docs, removeWords, stopwords("english"))
text.docs <- tm_map(text.docs, removePunctuation)
dtm <- DocumentTermMatrix(text.docs, control=list(weighting=weightTf))
m <- as.matrix(t(dtm))
v <- sort(rowSums(m),decreasing=TRUE)
d <- data.frame(word = names(v),freq=v)
set.seed(1234)
wordcloud(words = d$word, freq = d$freq, min.freq = 10,
          max.words=200, random.order=FALSE, rot.per=0.35,
          colors=brewer.pal(8, "Dark2"))

From the wordcloud we can see that words “color”, “hair” and “gloves” are quite frequent in the text corpus analyzed. That could be a hint that the user was referring to the usage of the product. The term “cheap” could be easily spotted as well. This word is not very likable among marketers as it brings unfavorable image to the brand. Nevertheless, it seems that the user believes that the product is affordable.

Future work

This data set provides multiple possibility for the further analysis besides recommender systems. Here are some ideas what can be further done:

  • Sentiment analysis - Sentiment analysis can be done and scores (typically from -3 to +3) accompanied to each review description. That would tell us more about the sentiment that users have about the products reviewed.

  • Prediction of ratings - In case that we would have enough data (ratings) about one product, regardless of customers, it would be possible to develop a machine learning model which based on current features (e.g. price) and additional features (such as sentiment or words in the review) could predict the rating that one product might have.

  • Prediction of the sentiment - in the similar manner as the previous point, it would be useful to train a machine learning model to predict a sentiment that would hypotetically emerge in a reviewer.

  • Topic modeling - topic modeling is unsupervised machine learning technique that could help us identify topics which users discuss in the text of reviews.

Limitations

Limitation related to this data set and building a recommender system is the fact that the majority of users have left only one review:

table(as.data.frame(table(my_data$`review/userId`))$Freq)

Let us take a look which users left the most reviews:

limitations <-as.data.frame(table(my_data$`review/userId`))
limitations %>% arrange(desc(Freq))%>%rename(UserID=Var1)%>% head()

We can see that users under IDs A3M174IC0VXOS2,A3KEZLJ59C1JVH,A3QEE0ZPMT3W6P are rare examples of users who left multiple product reviews.

LS0tDQp0aXRsZTogIjA3LVJlY29tbWVuZGF0aW9uX1N5c3RlbSINCm91dHB1dDoNCiAgaHRtbF9kb2N1bWVudDoNCiAgICB0b2M6IHllcw0KICAgIGRmX3ByaW50OiBwYWdlZA0KICBodG1sX25vdGVib29rOiBkZWZhdWx0DQogIHBkZl9kb2N1bWVudDoNCiAgICB0b2M6IHllcw0KLS0tDQoNCiMgKFBBUlQpIFJlY29tbWVuZGF0aW9uIHN5c3RlbSB7LX0NCg0KIyBSZWNvbW1lbmRhdGlvbiBzeXN0ZW0gDQoNCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVycm9yPSBGQUxTRSwgbWVzc2FnZSA9IEZBTFNFLCB3YXJuaW5nID0gRkFMU0UsIGZpZy5hbGlnbiA9ICJjZW50ZXIiKQ0KYGBgDQoNCmBgYHtyLGVjaG89RkFMU0UsIGZpZy5hbGlnbj0nY2VudGVyJywgb3V0LndpZHRoPSI1MCUiLGZpZy5jYXA9IkZvdG8gdm9uIEFuZHJlYSBQaWFjcXVhZGlvIHZvbiBQZXhlbHMifQ0Ka25pdHI6OmluY2x1ZGVfZ3JhcGhpY3MoIkdyYXBoaWNzL2JlYXV0eS5qcGciKQ0KYGBgDQoNCjxkaXYgc3R5bGU9InRleHQtYWxpZ246IGp1c3RpZnkiPiANCg0KQmFzZWQgb24gc29tZSBzdHVkaWVzIGl0IGhhcyBiZWVuIHByb3ZlbiB0aGF0IHBlcnNvbmFsaXplZCBwcm9kdWN0IHJlY29tbWVuZGF0aW9ucyBkcml2ZSAyNCUgb2YgdGhlIG9yZGVycyBhbmQgMjYlIG9mIHRoZSByZXZlbnVlLiBUaGlzIGV4cGxhaW5zIHRoZSBpbmZsdWVuY2UgcmVjb21tZW5kYXRpb24gaGFzIG9uIHZvbHVtZSBvZiBvcmRlcnMgYW5kIGdlbmVyYWxseSBvbiBzYWxlcyBmaWd1cmVzLiBXaGF0IGlzIG1vcmUsIGl0IGhhcyBiZWVuIHByb3ZlbiB0aGF0IHByb2R1Y3QgcmVjb21tZW5kYXRpb25zIGxlYWQgdG8gcmVvY2N1cnJpbmcgdmlzaXRzIGFuZCB0aGF0IHB1cmNoYXNlcyBvbiByZWNvbW1lbmRhdGlvbiBtYXJrIGhpZ2hlciBhdmVyYWdlLW9yZGVyIHZhbHVlLiBDb25zZXF1ZW50bHksIHdlIGRlY2lkZWQgdG8gdXNlIG1ldGhvZCBjYWxsZWQgdXNlci1iYXNlZCBjb2xsYWJvcmF0aXZlIGZpbHRlcmluZyB0byBidWlsZCBvdXIgcmVjb21tZW5kYXRpb24gc3lzdGVtICgqW1JlZmVyZW5jZV0oaHR0cHM6Ly93d3cuc2FsZXNmb3JjZS5jb20vYmxvZy8yMDE3LzExL3BlcnNvbmFsaXplZC1wcm9kdWN0LXJlY29tbWVuZGF0aW9ucy1kcml2ZS1qdXN0LTctdmlzaXRzLTI2LXJldmVudWUpKikuDQoNCkZpcnN0LCB3ZSBwcm9jZWVkIHdpdGggZGF0YSBwcmVwYXJhdGlvbiBhbmQgcHJlLXByb2Nlc3NpbmcsIHRoZW4gd2UgYnVpbGQgb3VyIHJlY29tbWVuZGVyIHN5c3RlbSwgYW5kIGZpbmFsbHkgZHJhdyBidXNpbmVzcyBpbXBsaWNhdGlvbnMuDQoNCjxkaXYgc3R5bGU9InRleHQtYWxpZ246IGp1c3RpZnkiPiANCg0KIyMgRGF0YSBjb2xsZWN0aW9uDQoNCkFzIHdlIGVhcmxpZXIgbWVudGlvbmVkLCB3ZSB1c2UgZGF0YSBvbiBBbWF6b24gY3VzdG9tZXIgcmV2aWV3cyBvZiBiZWF1dHkgcHJvZHVjdHMuIFRoZSBkYXRhIHVzZWQgaW4gdGhpcyBwcm9qZWN0IGNhbiBiZSBhY2Nlc3NlZCBpbiB0aGlzIFtsaW5rXShodHRwOi8vc25hcC5zdGFuZm9yZC5lZHUvZGF0YS93ZWItQW1hem9uLWxpbmtzLmh0bWwpLiBJdCBjb250YWlucyB0aGUgZm9sbG93aW5nIGZlYXR1cmVzOg0KDQpgYGB7ciBldmFsID0gVFJVRSwgZWNobyA9IEZBTFNFLCB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlID0gRkFMU0V9DQpsaWJyYXJ5KGRwbHlyKQ0KbGlicmFyeShrYWJsZUV4dHJhKQ0KbXl0YWJsZV9zdWIgPSBkYXRhLmZyYW1lKA0KICAgIFZhcmlhYmxlID0gYygiUHJvZHVjdCBwcmljZSIsDQogICAgICAgICAgICAgICAgICJQcm9kdWN0IElEIiwNCiAgICAgICAgICAgICAgICAgIlByb2R1Y3QgdGl0bGUiLA0KICAgICAgICAgICAgICAgICAiUmV2aWV3IGhlbHBmdWxuZXNzIiwNCiAgICAgICAgICAgICAgICAgIlByb2ZpbGUgbmFtZSIsDQogICAgICAgICAgICAgICAgICJSZXZpZXcgc2NvcmUiLA0KICAgICAgICAgICAgICAgICAiUmV2aWV3IHN1bW1hcnkiLA0KICAgICAgICAgICAgICAgICAiUmV2aWV3IHRleHQiLA0KICAgICAgICAgICAgICAgICAiUmV2aWV3IHRpbWUiLA0KICAgICAgICAgICAgICAgICAiUmV2aWV3IHVzZXJJZCIpLA0KICAgIERlc2NyaXB0aW9uID0gYygiSG93IG11Y2ggYSBwcm9kdWN0IGNvc3RzLiIsDQogICAgICAgICAgICAgICJBU0lOIG51bWJlciBvZiBhIHByb2R1Y3Qgb24gQW1hem9uLiIsDQogICAgICAgICAgICAgICJOYW1lIG9mIGEgcHJvZHVjdC4iLA0KICAgICAgICAgICAgICAiRnJhY3Rpb24gb2YgdXNlcnMgd2hvIGZvdW5kIHRoZSByZXZpZXcgaGVscGZ1bC4iLA0KICAgICAgICAgICAgICAiTmFtZSBvZiB0aGUgcHJvZmlsZSBvbiBBbWF6b24uIiwNCiAgICAgICAgICAgICAgIlJhdGluZyBvZiB0aGUgcHJvZHVjdC4iLA0KICAgICAgICAgICAgICAiQ29uY2lzZSBzdW1tYXJ5IG9mIHRoZSByZXZpZXcgdGV4dC4iLA0KICAgICAgICAgICAgICAiUmV2aWV3IHRleHQuIiwNCiAgICAgICAgICAgICAgIlJldmlldyB0aW1lLiIsDQogICAgICAgICAgICAgICJSZXZpZXcgdXNlcklkIikpDQoNCm15dGFibGVfc3ViICU+JSBrYWJsZShlc2NhcGUgPSBUKSAlPiUNCiAga2FibGVfcGFwZXIoYygiaG92ZXIiKSwgZnVsbF93aWR0aCA9IEYpICU+JQ0KICBmb290bm90ZShnZW5lcmFsID0gIiBJbmZvcm1hdGlvbiBjb2xsZWN0ZWQgZnJvbSBodHRwOi8vc25hcC5zdGFuZm9yZC5lZHUvZGF0YS93ZWItQW1hem9uLWxpbmtzLmh0bWwiLA0KICAgICAgICAgICBnZW5lcmFsX3RpdGxlID0gIk5vdGU6ICIsIA0KICAgICAgICAgICBmb290bm90ZV9hc19jaHVuayA9IFQsIHRpdGxlX2Zvcm1hdCA9IGMoIml0YWxpYyIpDQogICAgICAgICAgICkgDQpgYGANCg0KIyMgRGF0YSBwcmVwYXJhdGlvbiBhbmQgcHJlcHJvY2Vzc2luZw0KDQojIyMgUGFja2FnZXMNCg0KYGBge3Isd2FybmluZz1GQUxTRSxlcnJvcj1GQUxTRSxtZXNzYWdlPUZBTFNFfQ0KI1BhY2thZ2VzDQpsaWJyYXJ5KFIudXRpbHMpDQpsaWJyYXJ5KGRwbHlyKQ0KbGlicmFyeSh0aWR5cikNCmxpYnJhcnkoamFuaXRvcikNCmxpYnJhcnkocmVjb21tZW5kZXJsYWIpDQpsaWJyYXJ5KHRtKQ0KbGlicmFyeShOTFApDQpsaWJyYXJ5KHFkYXApDQpsaWJyYXJ5KHJlYWRyKQ0KbGlicmFyeSh3b3JkY2xvdWQpDQpgYGANCg0KIyMjIERhdGEgY29sbGVjdGlvbg0KDQpBZnRlciBkb3dubG9hZGluZyBkYXRhIGxvY2FsbHkgd2UgbG9hZCBpbiBkYXRhIGJ5IHVzaW5nYHJlYWRMaW5lcygpYCBmdW5jdGlvbjoNCg0KYGBge3IsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0UsIGV2YWw9VFJVRX0NCiMgTG9hZGluZyBpbiBkYXRhDQpteV9kYXRhIDwtIHJlYWRSRFMoImRhdGEvYW1hem9uX2JlYXV0eV9mdWxsLlJEUyIpDQpgYGANCg0KDQpMZXQgdXMgZmlyc3QgaGF2ZSBhIGxvb2sgYXQgdGhlIGRpbWVuc2lvbiBvZiBvdXIgZGF0YS4gT3VyIGRhdGEgc2V0IGlzIGN1cnJlbnRseSBpbiBhIGZvcm0gb2YgYSBzaW5nbGUgdmVjdG9yIHdpdGggMjc3MjYxNiBlbGVtZW50cy4gT2J2aW91c2x5LCB0aGlzIGlzIG5vdCB0aGUgb3B0aW1hbCBmb3JtIG9mIHRoZSBkYXRhIHdlIHdvdWxkIGxpa2UgdG8gd29yayB3aXRoLiBUaGF0IGlzIHdoeSB3ZSBuZWVkIHRvIHdvcmsgYXJvdW5kIHRoaXMgZGF0YSBzZXQgdG8gbWFrZSBpdCBtb3JlIGNvbnZlbmllbnQgZm9yIGZ1cnRoZXIgYW5hbHlzaXMuIA0KDQpXaGF0IHdlIGNhbiBkbyBmaXJzdCBpcyB0byByZW1vdmUgYWxsIGZpZWxkcyB3aXRoIG5vIGNoYXJhY3RlcnM6DQoNCmBgYHtyLCB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFLCBldmFsPVRSVUV9DQpteV9kYXRhIDwtIG15X2RhdGFbc2FwcGx5KG15X2RhdGEsIG5jaGFyKSA+IDBdDQpgYGANCg0KVGhlbiB3ZSBjYW4gY29udmVydCBpdCB0byBkYXRhIGZyYW1lOg0KDQpgYGB7ciwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRSwgZXZhbD1UUlVFfQ0KbXlfZGF0YSA8LSBhcy5kYXRhLmZyYW1lKG15X2RhdGEpDQpjb2xuYW1lcyhteV9kYXRhKSA8LSAicHJvZHVjdCINCmBgYA0KDQpPbmUgb2YgdGhlIGNyaXRpY2FsIHN0ZXBzIGlzIHNlcGFyYXRpbmcgdGhlIGNvbHVtbiB0byBtdWx0aXBsZSBjb2x1bW5zOg0KDQpgYGB7ciwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRSwgZXZhbD1UUlVFfQ0KIyBTZXBhcmF0ZSBvbmUgY29sdW1uIHRvIHR3byAoIjoiIHNlcGFyYXRvcikNCm15X2RhdGEgPC0gc2VwYXJhdGUobXlfZGF0YSxjb2wgPSBwcm9kdWN0LCBpbnRvID0gYygiSW5mbyIsIlByb2R1Y3QiKSwgc2VwID0gIjoiKQ0KYGBgDQoNCkluc3BlY3RpbmcgZmlyc3QgMTAgdmFsdWVzOg0KDQpgYGB7ciwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRSwgZXZhbD1UUlVFfQ0KaGVhZChteV9kYXRhLDEwKQ0KYGBgDQoNClRoZSBkYXRhIHNldCBpcyBsb2FkZWQgaW4gLnR4dCBmb3JtYXQsIHdoaWNoIG1ha2VzIGl0IGEgYml0IGNoYWxsZW5naW5nIHRvIHdvcmsgd2l0aC4gSW4gdGhlIGZvbGxvd2luZyBzZWN0aW9ucyB3ZSB3aWxsIHVuZGVydGFrZSBkYXRhIG1hbmlwdWxhdGlvbiBpbiBvcmRlciB0byBicmluZyB0aGUgZGF0YSBzZXQgaW4gbW9yZSBzdWl0YWJsZSBmb3JtLiANCg0KRmlyc3QsIHdlIHdpbGwgY29udmVydCBpdCBmcm9tIHRoZSBjdXJyZW50IGxvbmctZm9ybWF0IHRvIHRoZSB3aWRlLWZvcm1hdCwgd2hlcmUgZWFjaCBjb2x1bW4gd2lsbCByZXByZXNlbnQgYSBwcm9kdWN0LCBhbmQgZWFjaCByb3cgYSBmZWF0dXJlOg0KDQpgYGB7ciwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRSwgZXZhbD1UUlVFfQ0KI0NvbnZlcnRpbmcgbG9uZyBmb3JtYXQgdG8gd2lkZQ0KbXlfZGF0YSA8LSBteV9kYXRhICU+JQ0KICBncm91cF9ieShJbmZvKSAlPiUNCiAgbXV0YXRlKE9yZGVyID0gc2VxX2Fsb25nKEluZm8pKSAlPiUNCiAgc3ByZWFkKGtleSA9IE9yZGVyLCB2YWx1ZSA9IFByb2R1Y3QpDQpgYGANCg0KU2luY2UgdGhlIGNvbHVtbiBuYW1lcyBhcmUgbGFiZWxlZCB3aXRoIG51bWJlcnMsIHdlIHdpbGwgYXBwbHkgZmlyc3Qgcm93IGFzIGEgbGFiZWwgZm9yIHRoZSBjb3JyZXNwb25kaW5nIGNvbHVtbiBuYW1lOg0KDQpgYGB7ciwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRSwgZXZhbD1UUlVFfQ0KbXlfZGF0YSA8LSBhcy5kYXRhLmZyYW1lKHQobXlfZGF0YSkpDQpteV9kYXRhPC1teV9kYXRhJT4lDQogIHJvd190b19uYW1lcyhyb3dfbnVtYmVyID0gMSkNCm15X2RhdGENCmBgYA0KDQoNCkRlbGV0ZSByb3dzIHdpdGggYXQgbGVhc3QgMSBOQXM6DQoNCmBgYHtyLCB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFLCBldmFsPVRSVUV9DQpteV9kYXRhIDwtIG15X2RhdGFbcm93U3Vtcyhpcy5uYShteV9kYXRhKSk9PTAsXQ0KYGBgDQoNClRyaW0gd2hpdGUgc3BhY2UgYXQgdGhlIGJlZ2lubmluZyBvciBlbmRpbmcgdGhlIHN0cmluZzoNCg0KYGBge3IsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0UsIGV2YWw9VFJVRX0NCm15X2RhdGEkYHJldmlldy91c2VySWRgPC0gdHJpbXdzKG15X2RhdGEkYHJldmlldy91c2VySWRgKQ0KbXlfZGF0YSRgcHJvZHVjdC9wcm9kdWN0SWRgPC0gdHJpbXdzKG15X2RhdGEkYHByb2R1Y3QvcHJvZHVjdElkYCkNCm15X2RhdGEkYHByb2R1Y3QvcHJpY2VgPC0gdHJpbXdzKG15X2RhdGEkYHByb2R1Y3QvcHJpY2VgKQ0KbXlfZGF0YSRgcHJvZHVjdC90aXRsZWA8LSB0cmltd3MobXlfZGF0YSRgcHJvZHVjdC90aXRsZWApDQpgYGANCg0KDQpGaWx0ZXJpbmcgb3V0IHJldmlld3Mgd2l0aCB1bmtub3duIHVzZXJJRCBhbmQgcHJvZHVjdElkOg0KDQpgYGB7ciwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRSwgZXZhbD1UUlVFfQ0KbXlfZGF0YTwtZmlsdGVyKG15X2RhdGEsYHJldmlldy91c2VySWRgIT0idW5rbm93biIgJiBgcHJvZHVjdC9wcm9kdWN0SWRgIT0idW5rbm93biIgJiBgcHJvZHVjdC9wcmljZWAhPSJ1bmtub3duIikNCm15X2RhdGENCmBgYA0KDQpDb3JyZWN0aW5nIGNvbHVtbiBjbGFzc2VzOg0KDQpgYGB7ciwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRSwgZXZhbD1UUlVFfQ0KbXlfZGF0YSRgcHJvZHVjdC9wcm9kdWN0SWRgIDwtIGFzLmZhY3RvcihteV9kYXRhJGBwcm9kdWN0L3Byb2R1Y3RJZGApDQpgYGANCg0KYGBge3IsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0UsIGV2YWw9VFJVRX0NCm15X2RhdGEkYHJldmlldy9zY29yZWA8LSBhcy5udW1lcmljKG15X2RhdGEkYHJldmlldy9zY29yZWApDQpteV9kYXRhJGByZXZpZXcvdXNlcklkYDwtYXMuZmFjdG9yKG15X2RhdGEkYHJldmlldy91c2VySWRgKQ0KbXlfZGF0YSRgcHJvZHVjdC9wcmljZWA8LWFzLm51bWVyaWMobXlfZGF0YSRgcHJvZHVjdC9wcmljZWApDQp0YWJsZShteV9kYXRhJGByZXZpZXcvc2NvcmVgKQ0KYGBgDQoNCg0KIyMjIEhvdyBtYW55IHRpbWVzIHVzZXJzIHJldmlld2VkIHByb2R1Y3RzPw0KDQpJbiBvcmRlciB0byB1c2UgcmVsZXZhbnQgZGF0YSwgd2Ugd291bGQgbmVlZCB0byBkZWZpbmUgdGhlIG1pbmltdW0gbnVtYmVyIG9mIHJldmlld3MgcGVyIHVzZXIuIFNpbmNlIG1ham9yaXR5IG9mIHVzZXJzIGxlZnQgb25seSBvbmUgcmV2aWV3LiBUaGVyZWZvcmUsIHdlIHdpbGwgcmVtb3ZlIGFsbCBzaW5nbGUtcmV2aWV3IHVzZXJzIGFuZCBhbGwgb3RoZXIgdXNlcnMgd2hvIGxlZnQgbGVzcyB0aGVuIDIgcmV2aWV3cy4NCg0KRmlsdGVyaW5nIG91dCB1c2VycyB3aG8gbGVmdCAyIG9yIG1vcmUgcmV2aWV3czoNCmBgYHtyLCB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFLCBldmFsPUZBTFNFfQ0KZnJlcTwtYXMuZGF0YS5mcmFtZSh0YWJsZShteV9kYXRhJGByZXZpZXcvdXNlcklkYCkpDQpmcmVxDQppbmRleDwtZmlsdGVyKGZyZXEsIGZyZXEkRnJlcT4xKSRWYXIxDQpgYGANCg0KV2UgYXJlIG5vdyBsZWZ0IHdpdGggMTMxNiB1c2VycyB3aG8gcmV2aWV3ZWQgY2VydGFpbiBiZWF1dHkgcHJvZHVjdCBhdCBsZWFzdCAyIHRpbWVzLg0KDQpgYGB7ciwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRSwgZXZhbD1UUlVFfQ0KI215X2RhdGEgPC0gc3Vic2V0KG15X2RhdGEsYHJldmlldy91c2VySWRgICVpbiUgaW5kZXgpDQpteV9kYXRhDQpgYGANCg0KIyMgRXhwbG9yYXRvcnkgZGF0YSBhbmFseXNpcw0KDQojIyMgSGVhZCBvZiBkYXRhDQoNCmBgYHtyfQ0KaGVhZChteV9kYXRhKQ0KYGBgDQoNCg0KIyMjIEhvdyBtYW55IHVuaXF1ZSBwcm9kdWN0cyBhcmUgcmV2aWV3ZWQ/DQoNCmBgYHtyLCB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFfQ0KbGVuZ3RoKHVuaXF1ZShteV9kYXRhJGBwcm9kdWN0L3Byb2R1Y3RJZGApKQ0KYGBgDQoNClRoZXJlIGFyZSBgciBsZW5ndGgodW5pcXVlKG15X2RhdGEkJ3Byb2R1Y3QvcHJvZHVjdElkJykpYCBwcm9kdWN0cyB3aGljaCB3ZXJlIHJldmlld2VkLg0KDQoNCiMjIyBIb3cgbWFueSByZXZpZXdlcnMgZG8gd2UgaGF2ZT8NCg0KYGBge3IsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0V9DQpsZW5ndGgodW5pcXVlKG15X2RhdGEkYHJldmlldy91c2VySWRgKSkNCmBgYA0KDQpUaGVyZSBhcmUgYHIgbGVuZ3RoKHVuaXF1ZShteV9kYXRhJCdyZXZpZXcvdXNlcklkJykpYCB1bmlxdWUgcmV2aWV3ZXJzL2N1c3RvbWVycyB3aG8gcmV2aWV3ZWQgcHJvZHVjdHMuDQoNCg0KIyMjIEhvdyBtYW55IHNjb3JlcyBkbyB3ZSBoYXZlPw0KDQpgYGB7cn0NCmxlbmd0aChteV9kYXRhJGByZXZpZXcvc2NvcmVgKQ0KYGBgDQpUaGVyZSBhcmUgYHIgbGVuZ3RoKG15X2RhdGEkJ3Jldmlldy9zY29yZScpYCByYXRpbmdzLg0KDQojIyMgV2hhdCBpcyB0aGUgZGlzdHJpYnV0aW9uIG9mIHJhdGluZ3M/DQoNCmBgYHtyfQ0KaGlzdChhcy5udW1lcmljKG15X2RhdGEkYHJldmlldy9zY29yZWApLG1haW4gPSAiSGlzdG9ncmFtbSBvZiBzY29yZXMiLHhsYWIgPSAiU2NvcmUiKQ0KYGBgDQoNCg0KUHJvZHVjdHMgc2VlbSB0byBiZSBmYXZvcmFibHkgcmF0ZWQgYXMgdGhlIGRpc3RyaWJ1dGlvbiBvZiBzY29yZXMgc2hvd2VzIHRoYXQgdGhlIGJlc3Qgc2NvcmUgaXMgdGhlIG1vc3QgZnJlcXVlbnQuDQoNCiMjIyBXaGF0IGlzIHRoZSBhdmVyYWdlIG51bWJlciBvZiByZXZpZXdzIHBlciB1c2VyPw0KDQpgYGB7cixtZXNzYWdlPUZBTFNFLHdhcm5pbmc9RkFMU0V9DQpteV9kYXRhICU+JSANCiAgZ3JvdXBfYnkoYHJldmlldy91c2VySWRgKSAlPiUNCiAgc3VtbWFyaXNlKEZyZXE9bigpKSU+JSANCiAgc2VsZWN0KEZyZXEpICU+JSANCiAgc3VtbWFyeSgpDQpgYGANCkluIHRoZSBvcmlnaW5hbCBkYXRhIHNldCBJdCB1c2VycyBsZWZ0IG9uIGF2ZXJhZ2UgbGVmdCBhIHJldmlldyBvbmx5IG9uY2UuIEFmdGVyIGZpbHRlcmluZywgd2Ugc2VlIHRoYXQgb3VyIGF2ZXJhZ2UgaXMgYXQgMyByZXZpZXdzIHBlciB1c2VyLiANCg0KIyMjIFdoYXQgaXMgdGhlIGF2ZXJhZ2Ugc2NvcmUgcGVyIHVzZXI/DQoNCmBgYHtyLG1lc3NhZ2U9RkFMU0Usd2FybmluZz1GQUxTRX0NCihncmFuZC5tZWFuIDwtIG15X2RhdGEgJT4lIA0KIGRwbHlyOjpzdW1tYXJpc2UoR3JhbmQubWVhbj1tZWFuKGByZXZpZXcvc2NvcmVgKSkpDQpgYGANCkl0IHNlZW1zIHRoYXQgYmVhdXR5IHByb2R1Y3RzIG9uIEFtYXpvbiBhcmUgd2VsbCByZWNlaXZlZCBieSB1c2VycyBhcyB0aGUgYXZlcmFnZSBzY29yZSBwZXIgdXNlciBpcyBxdWl0ZSBoaWdoLCBhdCBgciBncmFuZC5tZWFuYC4gDQoNCiMjIEJ1aWxkaW5nIGEgbW9kZWwNCg0KIyMjIEZpbmFsIGRhdGEgb3V0bG9vaw0KDQpIZXJlIGlzIGEgZ2xpbXBzZSBpbiBvdXIgZGF0YSBiZWZvcmUgd2Ugc3RhcnQgYnVpbGRpbmcgdGhlIHJlY29tbW5kZXI6DQoNCmBgYHtyfQ0KaGVhZChteV9kYXRhKQ0KYGBgDQoNCiMjIyBTdWJzZXR0aW5nIGRhdGENCg0KSW4gb3JkZXIgdG8gbW9kZWwgYSByZWNvbW1lbmRlciBzeXN0ZW0sIHRocmVlIHZhcmlhYmxlcyBpbiBvdXIgY2FzZSBhcmUgb2YgZ3JlYXQgaW1wb3J0YW5jZToNCg0KKiBVc2VyIElEDQoqIFByb2R1Y3QgSUQNCiogU2NvcmUgLyBSYXRpbmcNCg0KT3VyIG1vZGVsIHdpbGwgYmUgYmFzZWQgb24gdGhlc2UgdGhyZWUgdmFyaWFibGVzLiBBZGRpdGlvbmFsbHksIHdlIHdpbGwgbWFrZSB1c2Ugb2YgdGhlIHJlbWFpbmluZyBmZWF0dXJlcyBieSB1dGlsaXppbmcgc29tZSB0ZXh0IG1pbmluZyB0ZWNobmlxdWVzLCBidXQgeW91IHdpbGwgZmluZCBtb3JlIGRldGFpbHMgYXQgc29tZSBsYXRlciBwb2ludC4NCk5vdywgd2Ugd2lsbCBtYWtlIGEgc3Vic2V0IG9mIG91ciBkYXRhIHdpdGggMyBtZW50aW9uZWQgdmFyaWFibGVzOg0KDQpgYGB7cn0NCnN1YnNldF9teV9kYXRhIDwtIHN1YnNldChteV9kYXRhLCBzZWxlY3QgPSBjKGByZXZpZXcvdXNlcklkYCxgcHJvZHVjdC9wcm9kdWN0SWRgLGByZXZpZXcvc2NvcmVgKSkNCnRhYmxlKHN1YnNldF9teV9kYXRhJGByZXZpZXcvc2NvcmVgKQ0KYGBgDQoNCkxldCB1cyBpbnNwZWN0IHRoZSBkaW1lbnNpb25zOg0KDQpgYGB7cn0NCmRpbShzdWJzZXRfbXlfZGF0YSkNCmBgYA0KDQojIyMgRm9ybWF0dGluZyBkYXRhDQoNCk91ciBkYXRhIGlzIGN1cnJlbnRseSBpbiB0aGUgbG9uZyBmb3JtYXQsIGkuZS4gb25lIHJvdyBmb3Igb25lIHJhdGluZy4gSG93ZXZlciwgd2Ugd291bGQgd2FudCB0byBnZXQgYSBtYXRyaXggd2l0aCByYXRpbmdzIHdoZXJlIHRoZSByb3dzIHJlcHJlc2VudCB0aGUgdXNlcnMgSURzIGFuZCB0aGUgY29sdW1ucyB0aGUgUHJvZHVjdCBJRHMuDQpUaHVzLCB3ZSB3aWxsIHRyYW5zZm9ybSBvdXIgZGF0YSB0byBzbyBjYWxsZWQgcmF0aW5nIG1hdHJpeDoNCg0KYGBge3J9DQpyYXRpbmdzIDwtIGFzKHN1YnNldF9teV9kYXRhLCAicmVhbFJhdGluZ01hdHJpeCIpDQpgYGANCg0KIyMjIEluc3BlY3RpbmcgcmVhbCByYXRpbmcgbWF0cml4DQpgYGB7cn0NCnZlY3Rvcl9yYXRpbmdzIDwtIGFzLnZlY3RvcihyYXRpbmdzQGRhdGEpDQp0YWJsZSh2ZWN0b3JfcmF0aW5ncykNCnZlY3Rvcl9yYXRpbmdzPC12ZWN0b3JfcmF0aW5nc1t2ZWN0b3JfcmF0aW5ncyAhPSBjKDAsNiw4LDksMTAsMTUsMTYsMjAsMjcpXQ0KIyBNb3N0IHJldmlld2VkIHByb2R1Y3RzDQpjb2xDb3VudHMocmF0aW5ncykNCiMgQXZlcmFnZSByYXRpbmcgcGVyIHByb2R1Y3QNCmNvbE1lYW5zKHJhdGluZ3MpDQoNCmBgYA0KDQpJbiBvcmRlciB0byBhdm9pZCAiaGlnaC9sb3cgcmF0aW5nIGJpYXMiIGZyb20gdXNlcnMgd2hvIGdpdmUgaGlnaCAob3IgbG93KSByYXRpbmdzIHRvIGFsbCB0aGUgcHJvZHVjdHMgdGhleSByZXZpZXdlZCwgd2Ugd2lsbCBuZWVkIHRvIG5vcm1hbGl6ZSBvdXIgZGF0YS4gVGhhdCB3b3VsZCBwcmV2ZW50IGNlcnRhaW4gYmlhcyBpbiB0aGUgcmVzdWx0cy4NCg0KYGBge3J9DQojcmF0aW5ncyA8LSBub3JtYWxpemUocmF0aW5ncykNCmBgYA0KDQpXZSBjYW4gcGxvdCBhbiBpbWFnZSBvZiB0aGUgcmF0aW5nIG1hdHJpeCBmb3IgdGhlIGZpcnN0IDI1MCB1c2VycyBhbmQgMjUwIHByb2R1Y3RzOg0KYGBge3J9DQppbWFnZShyYXRpbmdzWzE6MjUwLDE6MjUwXSkNCmBgYA0KDQpGcm9tIHRoZSB2aXN1YWxpc2F0aW9uIHdlIGNhbiBzZWUgdGhhdCByYXRpbmcgbWF0cml4IGlzIHZlcnkgc3BhcnNlLCBpLmUuIHRoYXQgbm90IGV2ZXJ5IHVzZXIgZGlkIHJhdGUvcmV2aWV3IGV2ZXJ5IHByb2R1Y3QgaW4gb3VyIGRhdGEgc2V0LiANCg0KV2UgY2FuIGluc3BlY3QgdGhlIGRhdGEgZm9yIHRoZSBmaXJzdCAxMCB1c2VycyBhbmQgdGhlIGZpcnN0IDQgcHJvZHVjdHM6DQoNCmBgYHtyfQ0KcmF0aW5nc1sxOjEwLCAxOjRdQGRhdGENCmBgYA0KDQpBcyB3ZSBhbHJlYWR5IHNhdyBpbiB0aGUgdmlzdWFsaXNhdGlvbiwgdGhlIGRhdGEgaXMgc3BhcnNlIGFuZCB0aGUgZmlyc3QgMTAgdXNlcnMgZGlkIG5vdCByZXZpZXcgZmlyc3QgNCBwcm9kdWN0cyB2aXN1YWxpc2VkIGluIHRoZSBtYXRyaXggYWJvdmUuDQoNCmBgYHtyfQ0KdmVjdG9yX3JhdGluZ3M8LWFzLnZlY3RvcihyYXRpbmdzQGRhdGEpDQp1bmlxdWUodmVjdG9yX3JhdGluZ3MpDQp0YWJsZSh2ZWN0b3JfcmF0aW5ncykNCmBgYA0KDQoNCiMjIyBDb21wYXJpbmcgbWV0aG9kcw0KDQpFdmFsdWF0aW5nIHJhdGluZ3MNCmBgYHtyfQ0Kbl9mb2xkPC01DQppdGVtc190b19rZWVwPC0xDQpyYXRpbmdfdGhyZXNob2xkIDwtIDMNCmV2YWxfc2V0cyA8LSBldmFsdWF0aW9uU2NoZW1lKGRhdGE9cmF0aW5ncyxtZXRob2Q9ImNyb3NzLXZhbGlkYXRpb24iLCBrPW5fZm9sZCwgZ2l2ZW49aXRlbXNfdG9fa2VlcCxnb29kUmF0aW5nPXJhdGluZ190aHJlc2hvbGQpDQpgYGANCg0KQ29tcGFyaW5nIG1ldGhvZHMNCmBgYHtyfQ0KbW9kZWxzX3RvX2V2YWwgPC0gbGlzdCgNCiAgVUJDRl9jb3MgPWxpc3QobmFtZT0iVUJDRiIscGFyYW09bGlzdChtZXRob2Q9ImNvc2luZSIpKSwNCiAgVUJDRl9jb3IgPWxpc3QobmFtZT0iVUJDRiIscGFyYW09bGlzdChtZXRob2Q9InBlYXJzb24iKSkpDQoNCm5fcmVjb21tZW5kYXRpb24gPC0gYygxLDUsMTAsMTAwKQ0KDQoNCmxpc3RfZXZhbCA8LSBldmFsdWF0ZShldmFsX3NldHMsbWV0aG9kID0gbW9kZWxzX3RvX2V2YWwsIG49bl9yZWNvbW1lbmRhdGlvbiApDQpgYGANCg0KDQojIyMgQnVpbGRpbmcgYSByZWNvbW1lbmRlcg0KDQpGaW5hbGx5LCB3ZSB3aWxsIG5vdyBidWlsZCBvdXIgcmVjb21tZW5kYXRpb24gc3lzdGVtIGJhc2VkIG9uICoqVXNlci1iYXNlZCBjb2xsYWJvcmF0aXZlIGZpbHRlcmluZyoqDQpVc2VyLWJhc2VkIGNvbGxhYm9yYXRpdmUgZmlsdGVyaW5nIHNlYXJjaCBmb3Igc2ltaWxhciB1c2VycyBhbmQgZ2l2ZXMgdGhlbSByZWNvbW1lbmRhdGlvbnMgYmFzZWQgb24gd2hhdCBvdGhlciB1c2VycyB3aXRoIHNpbWlsYXIgcmF0aW5nIHBhdHRlcm5zIGFwcHJlY2lhdGVkOg0KDQpgYGB7cix3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFfQ0KI0Nvc2luZQ0KcmVjb21tZW5kZXIgPC0gUmVjb21tZW5kZXIocmF0aW5ncywgbWV0aG9kPSJVQkNGIikNCnJlY29tbWVuZGVyDQpnZXRNb2RlbChyZWNvbW1lbmRlcikNCiNQZWFyc29uDQpyZWNvbW1lbmRlcl9wZWFyc29uIDwtUmVjb21tZW5kZXIocmF0aW5ncyxtZXRob2Q9IlVCQ0YiLHBhcmFtZXRlcj1saXN0KG1ldGhvZD0icGVhcnNvbiIpKQ0KcmVjb21tZW5kZXJfcGVhcnNvbg0KZ2V0TW9kZWwocmVjb21tZW5kZXJfcGVhcnNvbikNCg0KYGBgDQoNCkFkZGl0aW9uYWxseSwgaW4gb3JkZXIgdG8gY29tcGFyZSByZXN1bHRzIG9mIHR3byBtZXRob2RzLCAgd2Ugd291bGQgbGlrZSB0byBhcHBseSAqKml0ZW0tYmFzZWQgY29sbGFib3JhdGl2ZSBmaWx0ZXJpbmcqKiBtZXRob2QgdG8gYnVpbGQgYW5vdGhlciByZWNvbW1lbmRlciBzeXN0ZW0uIEluIGNvbnRyYXN0IHRvIHVzZXItYmFzZWQgY29sbGFib3JhdGl2ZSBmaWx0ZXJpbmcsIGl0ZW0tYmFzZWQgY29sbGFib3JhdGl2ZSBmaWx0ZXJpbmcgbG9va3MgZm9yIHNpbWlsYXJpdHkgcGF0dGVybnMgYmV0d2VlbiAqKml0ZW1zKiogYW5kIHJlY29tbWVuZHMgdGhlbSB0byB1c2VycyBiYXNlZCBvbiB0aGUgY29tcHV0ZWQgaW5mb3JtYXRpb24uDQoNCmBgYHtyLGV2YWw9VFJVRX0NCnJlY29tbWVuZGVySUJDRiA8LSBSZWNvbW1lbmRlcihyYXRpbmdzLCBtZXRob2Q9IklCQ0YiKQ0KcmVjb21tZW5kZXJJQkNGDQpgYGANCkFzIHJlcG9ydGVkLCBib3RoIHJlY29tbWVuZGF0aW9uIHN5c3RlbXMgYXJlIGJ1aWx0IHVzaW5nIDgwMDIgdXNlcnMuDQoNCg0KIyMgIEludGVycHJldGF0aW9uIGFuZCBtYW5hZ2VyaWFsIGltcGxpY2F0aW9ucw0KDQpOb3cgd2Ugd291bGQgbGlrZSB0byBpbnRlcnByZXQgdGhlIG91dHB1dCBvZiBvdXIgcmVjb21tZW5kZXIgc3lzdGVtcy4gDQpGaXJzdCB3ZSBzdGFydCB3aXRoIFVCQ0YtYmFzZWQgcmVjb21tZW5kZXIgc3lzdGVtLg0KDQpgYGB7cn0NCmN1cnJlbnQudXNlciA8LSA0NQ0KcmVjb21tZW5kYXRpb25zIDwtIHByZWRpY3QocmVjb21tZW5kZXIsIGN1cnJlbnQudXNlciwgZGF0YSA9IHJhdGluZ3MsIG4gPSA1KQ0KYGBgDQoNCldlIGRlY2lkZWQgdG8gdGFrZSB1c2VyIG51bWJlciBgciBjdXJyZW50LnVzZXJgIGFuZCBpbnNwZWN0IDUgcmVjb21tZW5kYXRpb25zIHByb3ZpZGVkIHRvIGhpbS9oZXIuDQpOb3cgd2UgY2FuIGluc3BlY3Qgd2hhdCBvdXIgcmVjb21tZW5kYXRpb24gc3lzdGVtIHByb3ZpZGVkIGluIHRoZSBlbmQ6DQoNCmBgYHtyfQ0Kc3RyKHJlY29tbWVuZGF0aW9ucykNCmBgYA0KDQpXZSBjYW4gc2VlIHRoYXQgdGhlIHVzZXIgSUQgb2YgdGhlIHVzZXIgbnVtYmVyIGByIGN1cnJlbnQudXNlcmAgaXMgQTEwTjE5T0wwQ0tZRFYuDQpPdXIgc3lzdGVtIGZvdW5kIDIgcHJvZHVjdHMgdG8gcmVjb21tZW5kIHRvIHRoaXMgdXNlciwgYW5kIHdlIGNhbiBmaW5kIHByb2R1Y3QgaW5kZXggKDE3MywgNzcyKSBhcyB3ZWxsIGFzIHJhdGluZ3MgdGhhdCB0aGUgc3lzdGVtIGNhbGN1bGF0ZWQgZnJvbSB0aGUgcmF0aW5ncyBvZiB0aGUgY2xvc2VzdCB1c2VycyAoNSw1KS4NCg0KTGV0IHVzIGNyZWF0ZSBhIHByZWRpY3Rpb24gbWFkZSBieSBJQkNGLWJhc2VkIHJlY29tbWVuZGVyOg0KDQpgYGB7cixldmFsPVRSVUV9DQpyZWNvbW1lbmRhdGlvbnNJQkNGIDwtIHByZWRpY3QocmVjb21tZW5kZXJJQkNGLGN1cnJlbnQudXNlcixkYXRhID0gcmF0aW5ncywgbj01KQ0Kc3RyKHJlY29tbWVuZGF0aW9uc0lCQ0YpDQpgYGANCg0KV2Ugd2lsbCBpbnNwZWN0IHBvdGVudGlhbCByZWNvbW1lbmRlZCBwcm9kdWN0czoNCg0KYGBge3IsZXZhbD1UUlVFfQ0KaGVhZChhcyhyZWNvbW1lbmRhdGlvbnNJQkNGLCJsaXN0IikpDQpgYGANCg0KVW5mb3J0dW5hdGVseSwgb3VyIGl0ZW0tYmFzZWQgY29sbGFib3JhdGl2ZSBmaWx0ZXJpbmcgc3lzdGVtIGRpZCBub3QgZ2VuZXJhdGUgYW55IHJlY29tbWVuZGF0aW9uIGZvciB0aGUgdXNlciBudW1iZXIgYHIgY3VycmVudC51c2VyYC4NCg0KDQojIyMgSWRlbnRpZmljYXRpb24gb2YgdGhlIHJlY29tbWVuZGVkIHByb2R1Y3RzDQoNCkxldCB1cyBub3cgaWRlbnRpZnkgdGhlIHByb2R1Y3RzIHJlY29tbWVuZGVkIGJ5IFVCQ0YtYmFzZWQgcmVjb21tZW5kZXIuIEZpcnN0IHdlIG5lZWQgdG8gZXh0cmFjdCB0aGUgaW5kZXggb2YgdGhlIHJlY29tbWVuZGVkIHByb2R1Y3RzOiAgDQoNCmBgYHtyfQ0KaW5kZXg8LSBhcy52ZWN0b3IoYXMuZmFjdG9yKHVubGlzdChhcyhyZWNvbW1lbmRhdGlvbnMsICJsaXN0IikpKSkNCmBgYA0KDQpUaGVuIHdlIGZpbmQgY29ycmVzcG9uZGluZyBwcm9kdWN0IGluIG91ciBpbml0aWFsIGRhdGEgc2V0Og0KDQpgYGB7cn0NCihyZWNvbW1lbmRhdGlvbl8yNjwtbXlfZGF0YVttYXRjaChpbmRleCwgbXlfZGF0YSRgcHJvZHVjdC9wcm9kdWN0SWRgKSxdKSAgDQpgYGANCg0KVHdvIHByb2R1Y3RzIHJlY29tbWVuZGVkIGFyZSA6DQoNCiogYHIgcmVjb21tZW5kYXRpb25fMjYkJ3Byb2R1Y3QvdGl0bGUnWzFdYCAtICBmYWNpYWwgY2xlYW5zaW5nIGNyZWFtDQoqIGByIHJlY29tbWVuZGF0aW9uXzI2JCdwcm9kdWN0L3RpdGxlJ1syXWAgLSAgY29sb3IgZm9yIGhhaXINCg0KDQpMZXQgdXMgbm93IGluc3BlY3QgcHJvZHVjdHMgdGhhdCB0aGUgdXNlciBBMTBOMTlPTDBDS1lEViByYXRlZDoNCg0KYGBge3J9DQpteV9kYXRhW21hdGNoKCJBMTBOMTlPTDBDS1lEViIsbXlfZGF0YSRgcmV2aWV3L3VzZXJJZGApLF0NCmBgYA0KDQojIyMgSW1wbGljYXRpb25zDQoNCkFzIHdlIGNvdWxkIHNlZSwgdGhpcyB1c2VyIHJldmlld2VkIG9ubHkgb25lIHByb2R1Y3QsIGNhbGxlZCAiT3BpIFJpZGdlIEZpbGxlciAuNSBvei4iLCBhbmQgaXQgaXMgYSBuYWlsLWNhcmUgcHJvZHVjdC4gV2UgY291bGQgYXNzdW1lIHRoYXQgdGhpcyBwZXJzb24gaXMgYSBmZW1hbGUgdXNlciBzaW5jZSB0aGUgcHJvZHVjdCBzaGUgYm91Z2h0IGlzIHR5cGljYWxseSBhc3NvY2lhdGVkIHdpdGggZmVtYWxlIGJlYXV0eSBjYXJlLiBXaGF0IGlzIG1vcmUsIHR3byByZWNvbW1lbmRlZCBwcm9kdWN0cyBhcmUgYXMgd2VsbCB2ZXJ5IHN0cm9uZ2x5IGFzc29jaWF0ZWQgdG8gYmVpbmcgdHlwaWNhbCBmZW1hbGUgYmVhdXR5IHByb2R1Y3RzLiBGaW5hbGx5LCB3ZSBoYXZlIHRoZSBuYW1lIG9mIHRoZSB1c2VyIChFcmljYSksIHNvIHdlIGNhbiBiZSBzdXJlIHRoYXQgdGhlIHVzZXIgaXMgYSBmZW1hbGUuDQpGcm9tIHRoZSBxdWFsaXRhdGl2ZSBwZXJzcGVjdGl2ZSBpdCBzZWVtcyB0aGF0IG91ciByZWNvbW1lbmRhdGlvbiBzeXN0ZW0gcHJvdmlkZXMgZGVzY2VudCByZWNvbW1lbmRhdGlvbnMhLg0KDQoNCiMjIEJvbnVzIGFuYWx5c2lzOiBUZXh0IE1pbmluZw0KDQpJbiBhZGRpdGlvbiB0byBvdXIgcmVjb21tZW5kZXIgc3lzdGVtLCB3ZSB3aWxsIGFwcGx5IHNvbWUgYmFzaWMgdGV4dCBtaW5pbmcgdGVjaG5pcXVlcyB0byBleHBsb3JlIHJldmlld3MgdGV4dC4gVGV4dCBtaW5pbmcgaGVscHMgdXMgdG8gbWluZSBvcGluaW9ucyBvZiB1c2VycyAoaW4gdGhpcyBjYXNlKSBhYm91dCB0aGUgcmV2aWV3ZWQgcHJvZHVjdHMgYXQgc2NhbGUuDQoNCiMjIyBXb3JkY2xvdWQgDQoNCkhlcmUgd2UgY3JlYXRlIGEgd29yZGNsb3VkIG9mIHdvcmRzIGZyb20gcHJvZHVjdCByZXZpZXdzIG9mIHJlY29tbWVuZGVkIHByb2R1Y3RzIHRvIHRoZSB1c2VyIGByIGN1cnJlbnQudXNlcmAuIEJlZm9yZWhhbmQgd2Ugd291bGQgbmVlZCB0byBwcmUtcHJvY2VzcyB0aGUgdGV4dCBvZiByZXZpZXdzIGluIHRoZSBmb2xsb3dpbmcgbWFubmVyOiANCg0KYGBge3Isd2FybmluZz1GQUxTRSxtZXNzYWdlPUZBTFNFfQ0KIyBTcGxpdCB0ZXh0IGludG8gcGFydHMgdXNpbmcgbmV3IGxpbmUgY2hhcmFjdGVyOg0KdGV4dC5kb2NzIDwtIENvcnB1cyhWZWN0b3JTb3VyY2UocmVjb21tZW5kYXRpb25fMjYkYHJldmlldy90ZXh0YCkpDQp0b1NwYWNlIDwtIGNvbnRlbnRfdHJhbnNmb3JtZXIoZnVuY3Rpb24gKHggLCBwYXR0ZXJuICkgZ3N1YihwYXR0ZXJuLCAiICIsIHgpKQ0KdGV4dC5kb2NzIDwtIHRtX21hcCh0ZXh0LmRvY3MsIHRvU3BhY2UsICIvIikNCnRleHQuZG9jcyA8LSB0bV9tYXAodGV4dC5kb2NzLCB0b1NwYWNlLCAiQCIpDQp0ZXh0LmRvY3MgPC0gdG1fbWFwKHRleHQuZG9jcywgdG9TcGFjZSwgIlxcfCIpDQp0ZXh0LmRvY3MgPC0gdG1fbWFwKHRleHQuZG9jcywgY29udGVudF90cmFuc2Zvcm1lcih0b2xvd2VyKSkNCnRleHQuZG9jcyA8LSB0bV9tYXAodGV4dC5kb2NzLCByZW1vdmVOdW1iZXJzKQ0KdGV4dC5kb2NzIDwtIHRtX21hcCh0ZXh0LmRvY3MsIHN0cmlwV2hpdGVzcGFjZSkNCnRleHQuZG9jcyA8LSB0bV9tYXAodGV4dC5kb2NzLCByZW1vdmVXb3Jkcywgc3RvcHdvcmRzKCJlbmdsaXNoIikpDQp0ZXh0LmRvY3MgPC0gdG1fbWFwKHRleHQuZG9jcywgcmVtb3ZlUHVuY3R1YXRpb24pDQpkdG0gPC0gRG9jdW1lbnRUZXJtTWF0cml4KHRleHQuZG9jcywgY29udHJvbD1saXN0KHdlaWdodGluZz13ZWlnaHRUZikpDQptIDwtIGFzLm1hdHJpeCh0KGR0bSkpDQp2IDwtIHNvcnQocm93U3VtcyhtKSxkZWNyZWFzaW5nPVRSVUUpDQpkIDwtIGRhdGEuZnJhbWUod29yZCA9IG5hbWVzKHYpLGZyZXE9dikNCnNldC5zZWVkKDEyMzQpDQp3b3JkY2xvdWQod29yZHMgPSBkJHdvcmQsIGZyZXEgPSBkJGZyZXEsIG1pbi5mcmVxID0gMTAsDQogICAgICAgICAgbWF4LndvcmRzPTIwMCwgcmFuZG9tLm9yZGVyPUZBTFNFLCByb3QucGVyPTAuMzUsDQogICAgICAgICAgY29sb3JzPWJyZXdlci5wYWwoOCwgIkRhcmsyIikpDQpgYGANCg0KRnJvbSB0aGUgd29yZGNsb3VkIHdlIGNhbiBzZWUgdGhhdCB3b3JkcyAiY29sb3IiLCAiaGFpciIgYW5kICJnbG92ZXMiIGFyZSBxdWl0ZSBmcmVxdWVudCBpbiB0aGUgdGV4dCBjb3JwdXMgYW5hbHl6ZWQuIFRoYXQgY291bGQgYmUgYSBoaW50IHRoYXQgdGhlIHVzZXIgd2FzIHJlZmVycmluZyB0byB0aGUgdXNhZ2Ugb2YgdGhlIHByb2R1Y3QuDQpUaGUgdGVybSAiY2hlYXAiIGNvdWxkIGJlIGVhc2lseSBzcG90dGVkIGFzIHdlbGwuIFRoaXMgd29yZCBpcyBub3QgdmVyeSBsaWthYmxlIGFtb25nIG1hcmtldGVycyBhcyBpdCBicmluZ3MgdW5mYXZvcmFibGUgaW1hZ2UgdG8gdGhlIGJyYW5kLiBOZXZlcnRoZWxlc3MsIGl0IHNlZW1zIHRoYXQgdGhlIHVzZXIgYmVsaWV2ZXMgdGhhdCB0aGUgcHJvZHVjdCBpcyBhZmZvcmRhYmxlLg0KDQoNCiMjIEZ1dHVyZSB3b3JrDQoNClRoaXMgZGF0YSBzZXQgcHJvdmlkZXMgbXVsdGlwbGUgcG9zc2liaWxpdHkgZm9yIHRoZSBmdXJ0aGVyIGFuYWx5c2lzIGJlc2lkZXMgcmVjb21tZW5kZXIgc3lzdGVtcy4NCkhlcmUgYXJlIHNvbWUgaWRlYXMgd2hhdCBjYW4gYmUgZnVydGhlciBkb25lOg0KDQoqICoqU2VudGltZW50IGFuYWx5c2lzKiogLSBTZW50aW1lbnQgYW5hbHlzaXMgY2FuIGJlIGRvbmUgYW5kIHNjb3JlcyAodHlwaWNhbGx5IGZyb20gLTMgdG8gKzMpIGFjY29tcGFuaWVkIHRvIGVhY2ggcmV2aWV3IGRlc2NyaXB0aW9uLiBUaGF0IHdvdWxkIHRlbGwgdXMgbW9yZSBhYm91dCB0aGUgc2VudGltZW50IHRoYXQgdXNlcnMgaGF2ZSBhYm91dCB0aGUgcHJvZHVjdHMgcmV2aWV3ZWQuDQoNCiogKipQcmVkaWN0aW9uIG9mIHJhdGluZ3MqKiAtIEluIGNhc2UgdGhhdCB3ZSB3b3VsZCBoYXZlIGVub3VnaCBkYXRhIChyYXRpbmdzKSBhYm91dCBvbmUgcHJvZHVjdCwgcmVnYXJkbGVzcyBvZiBjdXN0b21lcnMsIGl0IHdvdWxkIGJlIHBvc3NpYmxlIHRvIGRldmVsb3AgYSBtYWNoaW5lIGxlYXJuaW5nIG1vZGVsIHdoaWNoIGJhc2VkIG9uIGN1cnJlbnQgZmVhdHVyZXMgKGUuZy4gcHJpY2UpIGFuZCBhZGRpdGlvbmFsIGZlYXR1cmVzIChzdWNoIGFzIHNlbnRpbWVudCBvciB3b3JkcyBpbiB0aGUgcmV2aWV3KSBjb3VsZCBwcmVkaWN0IHRoZSByYXRpbmcgdGhhdCBvbmUgcHJvZHVjdCBtaWdodCBoYXZlLg0KDQoqICoqUHJlZGljdGlvbiBvZiB0aGUgc2VudGltZW50KiogLSBpbiB0aGUgc2ltaWxhciBtYW5uZXIgYXMgdGhlIHByZXZpb3VzIHBvaW50LCBpdCB3b3VsZCBiZSB1c2VmdWwgdG8gdHJhaW4gYSBtYWNoaW5lIGxlYXJuaW5nIG1vZGVsIHRvIHByZWRpY3QgYSBzZW50aW1lbnQgdGhhdCB3b3VsZCBoeXBvdGV0aWNhbGx5IGVtZXJnZSBpbiBhIHJldmlld2VyLg0KDQoqICoqVG9waWMgbW9kZWxpbmcqKiAtIHRvcGljIG1vZGVsaW5nIGlzIHVuc3VwZXJ2aXNlZCBtYWNoaW5lIGxlYXJuaW5nIHRlY2huaXF1ZSB0aGF0IGNvdWxkIGhlbHAgdXMgaWRlbnRpZnkgdG9waWNzIHdoaWNoIHVzZXJzIGRpc2N1c3MgaW4gdGhlIHRleHQgb2YgcmV2aWV3cy4gDQoNCiMjIExpbWl0YXRpb25zDQoNCkxpbWl0YXRpb24gcmVsYXRlZCB0byB0aGlzIGRhdGEgc2V0IGFuZCBidWlsZGluZyBhIHJlY29tbWVuZGVyIHN5c3RlbSBpcyB0aGUgZmFjdCB0aGF0IHRoZSBtYWpvcml0eSBvZiB1c2VycyBoYXZlIGxlZnQgb25seSBvbmUgcmV2aWV3Og0KDQpgYGB7cn0NCnRhYmxlKGFzLmRhdGEuZnJhbWUodGFibGUobXlfZGF0YSRgcmV2aWV3L3VzZXJJZGApKSRGcmVxKQ0KYGBgDQoNCkxldCB1cyB0YWtlIGEgbG9vayB3aGljaCB1c2VycyBsZWZ0IHRoZSBtb3N0IHJldmlld3M6DQoNCmBgYHtyfQ0KbGltaXRhdGlvbnMgPC1hcy5kYXRhLmZyYW1lKHRhYmxlKG15X2RhdGEkYHJldmlldy91c2VySWRgKSkNCmxpbWl0YXRpb25zICU+JSBhcnJhbmdlKGRlc2MoRnJlcSkpJT4lcmVuYW1lKFVzZXJJRD1WYXIxKSU+JSBoZWFkKCkNCmBgYA0KDQpXZSBjYW4gc2VlIHRoYXQgdXNlcnMgdW5kZXIgSURzIEEzTTE3NElDMFZYT1MyLEEzS0VaTEo1OUMxSlZILEEzUUVFMFpQTVQzVzZQIGFyZSByYXJlIGV4YW1wbGVzIG9mIHVzZXJzIHdobyBsZWZ0IG11bHRpcGxlIHByb2R1Y3QgcmV2aWV3cy4NCg==